﻿using RockBus.DataContracts;
using RockBus.Logging.Interfaces;
using RockBus.Messages.Interfaces;
using RockBus.Subscription.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel.Dispatcher;
using System.Threading.Tasks;
using System.Xml;

namespace RockBus.Subscription.Evaluators
{
    public abstract class EvaluatorBase<TConfiguration, TEvaluationElement, TInfo, TDetail> : IEvaluator<TConfiguration>
        where TConfiguration : EvaluatorConfigurationBase<TEvaluationElement, TInfo, TDetail>
        where TEvaluationElement : EvaluationElement<TInfo, TDetail>
        where TInfo : EvaluationElementInfoBase<TDetail>
        where TDetail : Detail
    {
        protected EvaluatorBase(ILogger logger, IMessageFilterFactory messageFilterFactory)
        {
            this.Logger = logger;
            this.MessageFilterFactory = messageFilterFactory;
            this.MessageFilterTableDictionary = new ConcurrentDictionary<string, MessageFilterTableEx<IList<TInfo>>>();
        }

        private ILogger Logger { get; set; }

        private IMessageFilterFactory MessageFilterFactory { get; set; }

        private TConfiguration Configuration { get; set; }

        protected abstract string LogIdentifier { get; }

        /// <summary>
        /// PublisherId to MessageFilterTable
        /// </summary>
        private ConcurrentDictionary<string, MessageFilterTableEx<IList<TInfo>>> MessageFilterTableDictionary { get; set; }

        #region IEvaluator

        public virtual void Initialize(TConfiguration publisherConfiguration)
        {
            this.Configuration = publisherConfiguration;
            this.RefreshMessageFilterTables();
        }

        public virtual void Evaluate(IMessage messageWrapper, EvaluationResult evaluationResult)
        {
            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "{0}: Evaluating for message with action {1}",
                this.LogIdentifier,
                messageWrapper.Action);

            // EvaluationResult is not (yet) threadsafe, so let's not use Parallel here.
            //Parallel.ForEach(this.Configuration.EvaluationElementDictionary.Values.ToArray(), (ee) =>
            foreach (var ee in this.Configuration.EvaluationElementDictionary.Values.ToArray())
            {
                var filterTable = this.GetMessageFilterTable(ee);
                if (null == filterTable)
                {
                    this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "{0}: FilterTable was null Id {1}",
                        this.LogIdentifier,
                        ee.Id);
                }
                else
                {
                    ICollection<IList<TInfo>> matchResult = new List<IList<TInfo>>();
                    filterTable.GetMatchingValues(
                        messageWrapper.MessageBuffer, matchResult);

                    foreach (var infoList in matchResult)
                    {
                        foreach (var info in infoList)
                        {
                            evaluationResult.Add(info);
                        }
                    }
                }
            }
            //);
        }

        #endregion IEvaluator

        private IMessageFilterTable<IList<TInfo>> GetMessageFilterTable(TEvaluationElement evaluationElement)
        {
            MessageFilterTableEx<IList<TInfo>> mft = null;
            if (this.MessageFilterTableDictionary.TryGetValue(evaluationElement.Id, out mft))
            {
                return mft;
            }

            throw new ArgumentException(string.Format("Element with Id {0} not found in MessageFilterTable. Please verify the evaluator configurations.", evaluationElement.Id));
        }

        private void RefreshMessageFilterTables()
        {
            // Remove filterTables not present in the configuration
            foreach (var key in this.MessageFilterTableDictionary.Keys.ToArray())
            {
                if (!this.Configuration.EvaluationElementDictionary.ContainsKey(key))
                {
                    MessageFilterTableEx<IList<TInfo>> mft = null;
                    this.MessageFilterTableDictionary.TryRemove(key, out mft);
                }
            }

            // Rebuild (if necessary) the MessageFilterTable(s)
            Parallel.ForEach(this.Configuration.EvaluationElementDictionary.Values, (ee) =>
            {
                this.RefreshMessageFilterTable(ee);
            });
        }

        private void RefreshMessageFilterTable(TEvaluationElement ee)
        {
            MessageFilterTableEx<IList<TInfo>> filterTable = null;
            if (this.MessageFilterTableDictionary.TryGetValue(ee.Id, out filterTable) &&
                filterTable.Timestamp == ee.ModificationDateTimeUtc)
            {
                return;
            }

            // If the filterTable was stale, remove it and recreate
            this.MessageFilterTableDictionary.TryRemove(ee.Id, out filterTable);

            // Unfortunately cannot use GetOrAdd, because should not add if null
            filterTable = CreateFilterTable(ee);
            if (filterTable != null)
            {
                this.MessageFilterTableDictionary.TryAdd(ee.Id, filterTable);
            }
        }

        private MessageFilterTableEx<IList<TInfo>> CreateFilterTable(TEvaluationElement ee)
        {
            IDictionary<string, MessageFilter> messageFilterDictionary = new Dictionary<string, MessageFilter>();
            IDictionary<string, IList<TInfo>> infoDictionary = new Dictionary<string, IList<TInfo>>();

            XmlNamespaceManager xns = CreateXmlNamespaceManager(ee.NamespaceInfoDictionary.Values);
            foreach (TInfo info in ee.InfoDictionary.Values)
            {
                foreach (var filterId in info.FilterIds)
                {
                    MessageFilter mf = null;
                    if (messageFilterDictionary.ContainsKey(filterId))
                    {
                        mf = messageFilterDictionary[filterId];
                    }
                    else
                    {
                        FilterInfo fi = ee.FilterInfoDictionary[filterId];
                        mf = this.MessageFilterFactory.CreateMessageFilter(fi, xns, ee.FilterInfoDictionary);
                    }

                    if (null == mf)
                    {
                        continue;
                    }

                    messageFilterDictionary[filterId] = mf;

                    // only add subscription to the dictionary when it is not part of AND filter for the same element
                    if (!InAndFilter(ee, info, filterId))
                    {
                        if (infoDictionary.ContainsKey(filterId))
                        {
                            infoDictionary[filterId].Add(info);
                        }
                        else
                        {
                            infoDictionary.Add(filterId, new List<TInfo>() { info });
                        }
                    }
                }
            }

            MessageFilterTableEx<IList<TInfo>> mft = new MessageFilterTableEx<IList<TInfo>>(ee.ModificationDateTimeUtc);
            foreach (KeyValuePair<string, IList<TInfo>> kvp in infoDictionary)
            {
                mft.Add(messageFilterDictionary[kvp.Key], kvp.Value);
            }

            return mft;
        }

        private bool InAndFilter(TEvaluationElement element, TInfo info, string filterId)
        {
            foreach (string filter in info.FilterIds)
            {
                FilterInfo fi = element.FilterInfoDictionary[filter];
                if (fi.FilterType == FilterTypeEnum.And)
                {
                    if (!string.IsNullOrEmpty(fi.Filter1Id) && fi.Filter1Id == filterId)
                        return true;
                    if (!string.IsNullOrEmpty(fi.Filter2Id) && fi.Filter2Id == filterId)
                        return true;
                }
            }
            return false;
        }

        private XmlNamespaceManager CreateXmlNamespaceManager(IEnumerable<NamespaceInfo> namespaces)
        {
            var xmlNamespaces = new XPathMessageContext();
            foreach (var ni in namespaces)
            {
                xmlNamespaces.AddNamespace(ni.Prefix, ni.Namespace);
            }
            return xmlNamespaces;
        }
    }
}